Skip to content

feat(client): local-first cache, virtualized list, optimistic UI#2

Merged
maneeshaxyz merged 3 commits into
mainfrom
feat/client-side-cache-virtualization
Jun 15, 2026
Merged

feat(client): local-first cache, virtualized list, optimistic UI#2
maneeshaxyz merged 3 commits into
mainfrom
feat/client-side-cache-virtualization

Conversation

@Aravinda-HWK

Copy link
Copy Markdown
Collaborator

Summary

Implements the client-side optimization tier from the Email 2.0 proposal — Phase 2 (client cache + cache-first render) plus parts of Phase 5 (virtualization + optimistic UI). Pure frontend; no backend or IMAP protocol changes. Gives the most immediately visible wins: instant warm loads, smooth scrolling over large mailboxes, and instant actions.

What's included

  • IndexedDB cache (src/nonview/cache/db.ts, Dexie) — threads and message-bodies tables, scoped per account email so a shared browser never mixes mailboxes. Every read/write is failure-tolerant: a cache error can never break the app (IMAP remains the source of truth).
  • Cache-first rendering (DataContext) — inbox/sent/trash paint from IndexedDB before the network responds, then refresh in the background with no blocking spinner on a warm cache. A failed folder fetch no longer clobbers good cached rows with an empty list.
  • Cache-first message bodies (ThreadPage + getCachedMessages) — reopening a thread paints instantly, then revalidates; a revalidation error won't blank an already-painted body.
  • Optimistic UImarkAsRead / deleteThread update screen + cache instantly and reconcile with the server, rolling back on failure.
  • List virtualization (ThreadList, @tanstack/react-virtual) — only on-screen rows mount; smooth scroll over large mailboxes.

New deps: dexie, @tanstack/react-virtual.

Targets addressed (proposal §3)

  • Time to first inbox render (warm cache) < 200ms → cache-first paint
  • Scroll performance on large mailboxes → list virtualization
  • Optimistic actions (read/delete) → instant UI, reconciled after

Verification

  • npm run build passes
  • npm run typecheck clean for the new/changed TS files
  • Dev server boots and serves 200 with the new ESM deps

Not yet covered: full end-to-end login against a live mailbox (requires the backend running).

Out of scope (follow-up increments)

  • Phase 3 — delta sync (CONDSTORE/MODSEQ)
  • Phase 4 — realtime (IMAP IDLE → WebSocket)
  • Phase 1 — in-memory server cache + IMAP connection pooling
  • Phase 6 — service worker / offline + queued mutations

Implements the client-side optimization tier from the Email 2.0 proposal
(Phase 2 + parts of Phase 5): instant warm loads, smooth scrolling over
large mailboxes, and instant actions. No backend/protocol changes.

- IndexedDB cache (Dexie), scoped per account email, for mailbox-list
  rows and message bodies. All reads/writes are failure-tolerant so a
  cache error can never break the app.
- Cache-first rendering: inbox/sent/trash paint from IndexedDB before the
  network responds, then refresh in the background with no blocking
  spinner. A failed folder fetch no longer clobbers good cached rows.
- Cache-first message bodies: reopening a thread paints instantly, then
  revalidates; a revalidation error won't blank an already-painted body.
- Optimistic markAsRead/deleteThread with rollback on failure.
- Virtualized ThreadList via @tanstack/react-virtual.

Adds deps: dexie, @tanstack/react-virtual.
@Aravinda-HWK Aravinda-HWK requested a review from maneeshaxyz June 14, 2026 07:40
@Aravinda-HWK Aravinda-HWK self-assigned this Jun 14, 2026
@Aravinda-HWK Aravinda-HWK added the enhancement New feature or request label Jun 14, 2026
The inbox previously loaded only the newest 50 messages per folder with no
way to see older mail. Adds cursor-based pagination on top of the
virtualized list:

- DataContext tracks a per-folder `before` UID cursor, `hasMore`, and
  `loadingMore`, and exposes loadMore(role). loadAll records the first
  page's cursor; loadMore fetches the next page (de-duped) and appends to
  state + IndexedDB cache.
- ThreadList auto-loads the next page when the last row scrolls into view
  (react-virtual), with a footer spinner while fetching.
- Inbox/Sent/Trash wire loadMore for their role. Pagination is suppressed
  while a search query is active (client-side search only filters loaded
  rows). The backend already supported the `before` cursor + next_before.
…scroll

Replaces the append-on-scroll pager with classic page controls: a
"start–end of total" label and prev/next arrows above the list.

Backend:
- imap.ListMessages also returns the mailbox total (mbox.Messages);
  the messages endpoint includes it as `total` for the "of N" count.

Frontend:
- DataContext tracks page/total/pageLoading per folder plus a per-page
  `before` cursor history (ref), and exposes nextPage/prevPage that
  REPLACE the visible page (forward cursor discovered from next_before,
  back reuses the stored cursor). Only page 0 is mirrored to the cache.
- ThreadList renders the pager bar above the virtualized scroll area and
  resets scroll to top on page change.
- Inbox/Sent/Trash wire prev/next for their role; the pager is hidden
  while a search query is active (search filters the loaded page only).

@maneeshaxyz maneeshaxyz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@maneeshaxyz maneeshaxyz merged commit 543a004 into main Jun 15, 2026
6 checks passed
maneeshaxyz pushed a commit to maneeshaxyz/quicksilver that referenced this pull request Jun 15, 2026
…LK#2)

* feat(client): local-first cache, virtualized list, optimistic UI

Implements the client-side optimization tier from the Email 2.0 proposal
(Phase 2 + parts of Phase 5): instant warm loads, smooth scrolling over
large mailboxes, and instant actions. No backend/protocol changes.

- IndexedDB cache (Dexie), scoped per account email, for mailbox-list
  rows and message bodies. All reads/writes are failure-tolerant so a
  cache error can never break the app.
- Cache-first rendering: inbox/sent/trash paint from IndexedDB before the
  network responds, then refresh in the background with no blocking
  spinner. A failed folder fetch no longer clobbers good cached rows.
- Cache-first message bodies: reopening a thread paints instantly, then
  revalidates; a revalidation error won't blank an already-painted body.
- Optimistic markAsRead/deleteThread with rollback on failure.
- Virtualized ThreadList via @tanstack/react-virtual.

Adds deps: dexie, @tanstack/react-virtual.

* feat(client): infinite-scroll pagination beyond the first 50 messages

The inbox previously loaded only the newest 50 messages per folder with no
way to see older mail. Adds cursor-based pagination on top of the
virtualized list:

- DataContext tracks a per-folder `before` UID cursor, `hasMore`, and
  `loadingMore`, and exposes loadMore(role). loadAll records the first
  page's cursor; loadMore fetches the next page (de-duped) and appends to
  state + IndexedDB cache.
- ThreadList auto-loads the next page when the last row scrolls into view
  (react-virtual), with a footer spinner while fetching.
- Inbox/Sent/Trash wire loadMore for their role. Pagination is suppressed
  while a search query is active (client-side search only filters loaded
  rows). The backend already supported the `before` cursor + next_before.

* feat: Gmail-style paged navigation ("1–50 of N") instead of infinite scroll

Replaces the append-on-scroll pager with classic page controls: a
"start–end of total" label and prev/next arrows above the list.

Backend:
- imap.ListMessages also returns the mailbox total (mbox.Messages);
  the messages endpoint includes it as `total` for the "of N" count.

Frontend:
- DataContext tracks page/total/pageLoading per folder plus a per-page
  `before` cursor history (ref), and exposes nextPage/prevPage that
  REPLACE the visible page (forward cursor discovered from next_before,
  back reuses the stored cursor). Only page 0 is mirrored to the cache.
- ThreadList renders the pager bar above the virtualized scroll area and
  resets scroll to top on page change.
- Inbox/Sent/Trash wire prev/next for their role; the pager is hidden
  while a search query is active (search filters the loaded page only).
maneeshaxyz pushed a commit to maneeshaxyz/quicksilver that referenced this pull request Jun 15, 2026
…LK#2)

* feat(client): local-first cache, virtualized list, optimistic UI

Implements the client-side optimization tier from the Email 2.0 proposal
(Phase 2 + parts of Phase 5): instant warm loads, smooth scrolling over
large mailboxes, and instant actions. No backend/protocol changes.

- IndexedDB cache (Dexie), scoped per account email, for mailbox-list
  rows and message bodies. All reads/writes are failure-tolerant so a
  cache error can never break the app.
- Cache-first rendering: inbox/sent/trash paint from IndexedDB before the
  network responds, then refresh in the background with no blocking
  spinner. A failed folder fetch no longer clobbers good cached rows.
- Cache-first message bodies: reopening a thread paints instantly, then
  revalidates; a revalidation error won't blank an already-painted body.
- Optimistic markAsRead/deleteThread with rollback on failure.
- Virtualized ThreadList via @tanstack/react-virtual.

Adds deps: dexie, @tanstack/react-virtual.

* feat(client): infinite-scroll pagination beyond the first 50 messages

The inbox previously loaded only the newest 50 messages per folder with no
way to see older mail. Adds cursor-based pagination on top of the
virtualized list:

- DataContext tracks a per-folder `before` UID cursor, `hasMore`, and
  `loadingMore`, and exposes loadMore(role). loadAll records the first
  page's cursor; loadMore fetches the next page (de-duped) and appends to
  state + IndexedDB cache.
- ThreadList auto-loads the next page when the last row scrolls into view
  (react-virtual), with a footer spinner while fetching.
- Inbox/Sent/Trash wire loadMore for their role. Pagination is suppressed
  while a search query is active (client-side search only filters loaded
  rows). The backend already supported the `before` cursor + next_before.

* feat: Gmail-style paged navigation ("1–50 of N") instead of infinite scroll

Replaces the append-on-scroll pager with classic page controls: a
"start–end of total" label and prev/next arrows above the list.

Backend:
- imap.ListMessages also returns the mailbox total (mbox.Messages);
  the messages endpoint includes it as `total` for the "of N" count.

Frontend:
- DataContext tracks page/total/pageLoading per folder plus a per-page
  `before` cursor history (ref), and exposes nextPage/prevPage that
  REPLACE the visible page (forward cursor discovered from next_before,
  back reuses the stored cursor). Only page 0 is mirrored to the cache.
- ThreadList renders the pager bar above the virtualized scroll area and
  resets scroll to top on page change.
- Inbox/Sent/Trash wire prev/next for their role; the pager is hidden
  while a search query is active (search filters the loaded page only).
@Aravinda-HWK Aravinda-HWK deleted the feat/client-side-cache-virtualization branch June 21, 2026 08:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants